/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sentry.spring.boot;

import com.jakewharton.nopen.annotation.Open;

import io.sentry.EventProcessor;
import io.sentry.HubAdapter;
import io.sentry.IHub;
import io.sentry.ITransportFactory;
import io.sentry.Integration;
import io.sentry.Sentry;
import io.sentry.SentryOptions;
import io.sentry.protocol.SdkVersion;
import io.sentry.spring.SentryExceptionResolver;
import io.sentry.spring.SentryRequestResolver;
import io.sentry.spring.SentrySpringRequestListener;
import io.sentry.spring.SentryUserFilter;
import io.sentry.spring.SentryUserProvider;
import io.sentry.spring.SentryWebConfiguration;
import io.sentry.spring.SpringSecuritySentryUserProvider;
import io.sentry.spring.tracing.SentryAdviceConfiguration;
import io.sentry.spring.tracing.SentrySpanPointcutConfiguration;
import io.sentry.spring.tracing.SentryTracingFilter;
import io.sentry.spring.tracing.SentryTransactionPointcutConfiguration;
import io.sentry.transport.ITransportGate;
import io.sentry.transport.apache.ApacheHttpClientTransportFactory;

import java.util.List;
import java.util.Optional;

import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.ProceedingJoinPoint;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.info.GitProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.client.RestTemplate;

/**
 * SentryAutoConfiguration
 *
 * @since 2021-07-29
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "sentry.dsn")
@Open
public class SentryAutoConfiguration {
    /**
     * Registers general purpose Sentry related beans.
     */
    @Configuration(proxyBeanMethods = false)
    @EnableConfigurationProperties(SentryProperties.class)
    @Open
    static class HubConfiguration {
        /**
         * sentryOptionsConfiguration
         *
         * @param beforeSendCallback ObjectProvider<SentryOptions.BeforeSendCallback>
         * @param beforeBreadcrumbCallback ObjectProvider<SentryOptions.BeforeBreadcrumbCallback>
         * @param tracesSamplerCallback ObjectProvider<SentryOptions.TracesSamplerCallback>
         * @param eventProcessors List<EventProcessor>
         * @param integrations List<Integration>
         * @param transportGate ObjectProvider<ITransportGate>
         * @param transportFactory ObjectProvider<ITransportFactory>
         * @param inAppPackagesResolver ObjectProvider<ITransportFactory>
         * @return Sentry.OptionsConfiguration<SentryOptions>
         */
        @Bean
        @ConditionalOnMissingBean(name = "sentryOptionsConfiguration")
        @Order(Ordered.HIGHEST_PRECEDENCE)
        public @NotNull Sentry.OptionsConfiguration<SentryOptions> sentryOptionsConfiguration(
            final @NotNull ObjectProvider<SentryOptions.BeforeSendCallback> beforeSendCallback,
            final @NotNull ObjectProvider<SentryOptions.BeforeBreadcrumbCallback>
                beforeBreadcrumbCallback,
            final @NotNull ObjectProvider<SentryOptions.TracesSamplerCallback> tracesSamplerCallback,
            final @NotNull List<EventProcessor> eventProcessors,
            final @NotNull List<Integration> integrations,
            final @NotNull ObjectProvider<ITransportGate> transportGate,
            final @NotNull ObjectProvider<ITransportFactory> transportFactory,
            final @NotNull InAppIncludesResolver inAppPackagesResolver) {
            return options -> {
                beforeSendCallback.ifAvailable(options::setBeforeSend);
                beforeBreadcrumbCallback.ifAvailable(options::setBeforeBreadcrumb);
                tracesSamplerCallback.ifAvailable(options::setTracesSampler);
                eventProcessors.forEach(options::addEventProcessor);
                integrations.forEach(options::addIntegration);
                transportGate.ifAvailable(options::setTransportGate);
                transportFactory.ifAvailable(options::setTransportFactory);
                inAppPackagesResolver.resolveInAppIncludes().forEach(options::addInAppInclude);
            };
        }

        /**
         * inAppPackagesResolver
         *
         * @return InAppIncludesResolver
         */
        @Bean
        public @NotNull InAppIncludesResolver inAppPackagesResolver() {
            return new InAppIncludesResolver();
        }

        /**
         * sentryHub
         *
         * @param optionsConfigurations List<Sentry.OptionsConfiguration<SentryOptions>>
         * @param options SentryProperties
         * @param gitProperties ObjectProvider<GitProperties>
         * @return IHub
         */
        @Bean
        public @NotNull IHub sentryHub(
            final @NotNull List<Sentry.OptionsConfiguration<SentryOptions>> optionsConfigurations,
            final @NotNull SentryProperties options,
            final @NotNull ObjectProvider<GitProperties> gitProperties) {
            optionsConfigurations.forEach(
                optionsConfiguration -> optionsConfiguration.configure(options));
            gitProperties.ifAvailable(
                git -> {
                    if (options.getRelease() == null && options.isUseGitCommitIdAsRelease()) {
                        options.setRelease(git.getCommitId());
                    }
                });

            options.setSentryClientName(BuildConfig.SENTRY_SPRING_BOOT_SDK_NAME);
            options.setSdkVersion(createSdkVersion(options));
            if (options.getTracesSampleRate() == null) {
                options.setTracesSampleRate(0.0);
            }
            // Spring Boot sets ignored exceptions in runtime using reflection - where the generic
            // information is lost
            // its technically possible to set non-throwable class to `ignoredExceptionsForType` set
            // here we make sure that only classes that extend throwable are set on this field
            options.getIgnoredExceptionsForType().removeIf(it -> !Throwable.class.isAssignableFrom(it));
            Sentry.init(options);
            return HubAdapter.getInstance();
        }

        /**
         * Registers beans specific to Spring MVC.
         */
        @Configuration(proxyBeanMethods = false)
        @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
        @Import(SentryWebConfiguration.class)
        @Open
        static class SentryWebMvcConfiguration {

            /**
             * Configures {@link SpringSecuritySentryUserProvider} only if Spring Security is on the
             * classpath. Its order is set to be higher than {@link
             * SentryWebConfiguration#httpServletRequestSentryUserProvider(SentryOptions)}
             *
             * @param sentryOptions the Sentry options
             * @return {@link SpringSecuritySentryUserProvider}
             */
            @Bean
            @ConditionalOnClass(SecurityContextHolder.class)
            @Order(1)
            public @NotNull SpringSecuritySentryUserProvider springSecuritySentryUserProvider(
                final @NotNull SentryOptions sentryOptions) {
                return new SpringSecuritySentryUserProvider(sentryOptions);
            }

            /**
             * Configures {@link SentryUserFilter}. By default it runs as the last filter in order to make
             * sure that all potential authentication information is propagated to {@link
             * HttpServletRequest#getUserPrincipal()}. If Spring Security is auto-configured, its order is
             * set to run after Spring Security.
             *
             * @param hub the Sentry hub
             * @param sentryProperties the Sentry properties
             * @param sentryUserProvider the user provider
             * @return {@link SentryUserFilter} registration bean
             */
            @Bean
            @ConditionalOnBean(SentryUserProvider.class)
            public @NotNull FilterRegistrationBean<SentryUserFilter> sentryUserFilter(
                final @NotNull IHub hub,
                final @NotNull SentryProperties sentryProperties,
                final @NotNull List<SentryUserProvider> sentryUserProvider) {
                final FilterRegistrationBean<SentryUserFilter> filter = new FilterRegistrationBean<>();
                filter.setFilter(new SentryUserFilter(hub, sentryUserProvider));
                filter.setOrder(resolveUserFilterOrder(sentryProperties));
                return filter;
            }

            private @NotNull Integer resolveUserFilterOrder(
                final @NotNull SentryProperties sentryProperties) {
                return Optional.ofNullable(sentryProperties.getUserFilterOrder())
                    .orElse(Ordered.LOWEST_PRECEDENCE);
            }

            /**
             * sentryRequestResolver
             *
             * @param hub IHub
             * @return SentryRequestResolver
             */
            @Bean
            public @NotNull SentryRequestResolver sentryRequestResolver(final @NotNull IHub hub) {
                return new SentryRequestResolver(hub);
            }

            /**
             * sentrySpringRequestListener
             *
             * @param sentryHub IHub
             * @param requestResolver SentryRequestResolver
             * @return SentrySpringRequestListener
             */
            @Bean
            public @NotNull SentrySpringRequestListener sentrySpringRequestListener(
                final @NotNull IHub sentryHub, final @NotNull SentryRequestResolver requestResolver) {
                return new SentrySpringRequestListener(sentryHub, requestResolver);
            }

            /**
             * sentryExceptionResolver
             *
             * @param sentryHub IHub
             * @param options SentryProperties
             * @return SentryExceptionResolver
             */
            @Bean
            @ConditionalOnMissingBean
            public @NotNull SentryExceptionResolver sentryExceptionResolver(
                final @NotNull IHub sentryHub, final @NotNull SentryProperties options) {
                return new SentryExceptionResolver(sentryHub, options.getExceptionResolverOrder());
            }

            /**
             * sentryTracingFilter
             *
             * @param hub IHub
             * @return FilterRegistrationBean<SentryTracingFilter>
             */
            @Bean
            @Conditional(SentryTracingCondition.class)
            @ConditionalOnMissingBean(name = "sentryTracingFilter")
            public FilterRegistrationBean<SentryTracingFilter> sentryTracingFilter(
                final @NotNull IHub hub) {
                FilterRegistrationBean<SentryTracingFilter> filter =
                    new FilterRegistrationBean<>(new SentryTracingFilter(hub));
                filter.setOrder(Ordered.HIGHEST_PRECEDENCE);
                return filter;
            }
        }

        /**
         * SentryPerformanceAspectsConfiguration
         *
         * @since 2021-07-29
         */
        @Configuration(proxyBeanMethods = false)
        @Conditional(SentryTracingCondition.class)
        @ConditionalOnClass(ProceedingJoinPoint.class)
        @Import(SentryAdviceConfiguration.class)
        @Open
        static class SentryPerformanceAspectsConfiguration {
            /**
             * SentryTransactionPointcutAutoConfiguration
             *
             * @since 2021-07-29
             */
            @Configuration(proxyBeanMethods = false)
            @ConditionalOnMissingBean(name = "sentryTransactionPointcut")
            @Import(SentryTransactionPointcutConfiguration.class)
            @Open
            static class SentryTransactionPointcutAutoConfiguration {
            }

            /**
             * SentrySpanPointcutAutoConfiguration
             *
             * @since 2021-07-29
             */
            @Configuration(proxyBeanMethods = false)
            @ConditionalOnMissingBean(name = "sentrySpanPointcut")
            @Import(SentrySpanPointcutConfiguration.class)
            @Open
            static class SentrySpanPointcutAutoConfiguration {
            }
        }

        /**
         * SentryPerformanceRestTemplateConfiguration
         *
         * @since 2021-07-29
         */
        @Configuration(proxyBeanMethods = false)
        @AutoConfigureBefore(RestTemplateAutoConfiguration.class)
        @ConditionalOnClass(RestTemplate.class)
        @Open
        static class SentryPerformanceRestTemplateConfiguration {
            /**
             * sentrySpanRestTemplateCustomizer
             *
             * @param hub hub
             * @return SentrySpanRestTemplateCustomizer
             */
            @Bean
            public SentrySpanRestTemplateCustomizer sentrySpanRestTemplateCustomizer(IHub hub) {
                return new SentrySpanRestTemplateCustomizer(hub);
            }
        }

        /**
         * ApacheHttpClientTransportFactoryAutoconfiguration
         *
         * @since 2021-07-29
         */
        @Configuration(proxyBeanMethods = false)
        @ConditionalOnMissingBean(ITransportFactory.class)
        @ConditionalOnClass(ApacheHttpClientTransportFactory.class)
        @Open
        static class ApacheHttpClientTransportFactoryAutoconfiguration {
            /**
             * apacheHttpClientTransportFactory
             *
             * @return ApacheHttpClientTransportFactory
             */
            @Bean
            public @NotNull ApacheHttpClientTransportFactory apacheHttpClientTransportFactory() {
                return new ApacheHttpClientTransportFactory();
            }
        }

        private static @NotNull SdkVersion createSdkVersion(
            final @NotNull SentryOptions sentryOptions) {
            SdkVersion sdkVersion = sentryOptions.getSdkVersion();

            final String name = BuildConfig.SENTRY_SPRING_BOOT_SDK_NAME;
            final String version = BuildConfig.VERSION_NAME;
            sdkVersion = SdkVersion.updateSdkVersion(sdkVersion, name, version);

            sdkVersion.addPackage("maven:io.sentry:sentry-spring-boot-starter", version);

            return sdkVersion;
        }
    }

    /**
     * SentryTracingCondition
     *
     * @since 2021-07-29
     */
    static final class SentryTracingCondition extends AnyNestedCondition {
        /**
         * SentryTracingCondition
         */
        public SentryTracingCondition() {
            super(ConfigurationPhase.REGISTER_BEAN);
        }

        @ConditionalOnProperty(name = "sentry.enable-tracing", havingValue = "true")
        @SuppressWarnings("UnusedNestedClass")
        private static class SentryTracingEnabled {
        }

        @ConditionalOnProperty(name = "sentry.traces-sample-rate")
        @SuppressWarnings("UnusedNestedClass")
        private static class SentryTracesSampleRateCondition {
        }

        @ConditionalOnBean(SentryOptions.TracesSamplerCallback.class)
        @SuppressWarnings("UnusedNestedClass")
        private static class SentryTracesSamplerBeanCondition {
        }
    }
}
